#region
/* Adoor.NET - Object/Relational mapping framework leveraging ADO.NET`s powerful disconnected mode.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *  
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */ 
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using Adoor.Expression;
using Adoor.Expression.AST;
//using Adoor.Object.Design;
using Adoor.Object.Domain;
using Adoor.Object.Entity;

namespace Adoor.Object.Query
{
	/// <summary>
	/// A select query over an <see cref="ObjectDomain"/>.
	/// </summary>
	public class ObjectQuery: Component
	{
		public ObjectQuery()
		{
			parser = new ExpressionParser();
			parser.ParseSubQuery += new Adoor.Expression.ExpressionParser.ParseSubQueryEventHandler(ExpressionParser_ParseSubQuery);
            alUnionAllObjectQueries = new List<ObjectQuery>();
			this.virtualEntityData = new EntityData();
		}

		public ObjectQuery(IObjectResolver objectResolver, string text) : this()
		{
            this.objectResolver = objectResolver;
			this.Text = text;
		}

		public ObjectQuery(ObjectPath path):
			this(path, null)
		{
		}

		public ObjectQuery(ObjectPath path, SelectExpression[] selectColumns):
			this(path, selectColumns, false, null, Int64.MaxValue, null, null)
		{
		}

		public ObjectQuery(ObjectPath path, SelectExpression[] selectColumns, bool distinct, SortExpression[] sortOrder, Int64 topCount, GroupByExpression[] groupBy, HavingExpression having) : this()
		{
			this.path = path;
			this.objectResolver = path.ObjectResolver;
			this.selectColumns = selectColumns;
			this.distinct = distinct;
			this.sortOrder = sortOrder;
			this.topCount = topCount;
			this.having = having;
			this.groupBy = groupBy;
		}

		private void reinit()
		{
            alUnionAllObjectQueries = new List<ObjectQuery>();
			this.text = "";
			this.selectColumns = null;
			this.distinct = false;
			this.sortOrder = null;
			this.topCount = Int64.MaxValue;
			this.having = null;
			this.groupBy = null;
		}

        public EntityData VirtualEntityData
		{
			get{ return virtualEntityData;}
		}

		private EntityData virtualEntityData;

		//[Editor(typeof(QueryEditor), typeof(UITypeEditor))]
		public string Text
		{
			get { return this.text; }
			set
			{
				if (value == this.text)
					return;
//				if (this.domain == null)
//					throw new Exception("Domain is not set");
				this.text = value;
//				Parse();
			}
		}

		[Browsable(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public ObjectPath Path
		{
			get 
			{
				if (this.path == null && this.text != "")
					Parse();
				return this.path; 
			}
			set { this.path = value; }
		}

		[Browsable(false)]
		public SelectExpression[] SelectColumns
		{
			get { return this.selectColumns; }
			set { this.selectColumns = value; }
		}

		[Browsable(false)]
		public HavingExpression Having
		{
			get { return this.having; }
			set { this.having = value; }
		}

		[Browsable(false)]
		public GroupByExpression[] GroupBy
		{
			get { return this.groupBy; }
			set { this.groupBy = value; }
		}

		[Browsable(false)]
		[DefaultValue(false)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public bool Distinct
		{
			get { return this.distinct; }
			set { this.distinct = value; }
		}

		[Browsable(false)]
		[DefaultValue(null)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public SortExpression[] SortOrder
		{
			get { return this.sortOrder; }
			set { this.sortOrder = value; }
		}

		[Browsable(false)]
		[DefaultValue(Int64.MaxValue)]
		[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public Int64 TopCount
		{
			get { return this.topCount; }
			set { this.topCount = value; }
		}

		private List<ObjectQuery> alUnionAllObjectQueries;
		public ObjectQuery[] UnionAllObjectQueries
		{
			get 
			{
				return alUnionAllObjectQueries.ToArray(); 
			}			
		}

		private ExpressionParser parser;
		public void Parse()
		{			
			Scanner scanner = new Scanner(this.Text);
			try
			{
				scanner.Scan();
				Parse(scanner);
			}
			catch(Exception e)
			{
				int start = Math.Max(0, scanner.Pos - 20);
				int length = scanner.Pos - start;
				string after = this.Text.Substring(start, Math.Min(length, this.Text.Length - start));
				throw new ParseException(e.Message + " after " + after + "  (pos = " + scanner.Pos.ToString() + ")", e, scanner.Pos, scanner.Text);
			}
		}

		public void Parse(Scanner scanner, params ScannerToken[] endTokens)
		{
			switch(scanner.Token)
			{
				default:
				case ScannerToken.Select:
					ParseSelect(scanner, endTokens);
					break;
				case ScannerToken.Insert:
					ParseInsert(scanner, endTokens);
					break;
				case ScannerToken.Update:
					ParseUpdate(scanner, endTokens);
					break;
				case ScannerToken.Delete:
					ParseDelete(scanner, endTokens);
					break;
			}			
		}

		public void ParseInsert(Scanner scanner, params ScannerToken[] endTokens)
		{
			queryType = QueryType.Insert;
			scanner.Scan();
			if(scanner.Token != ScannerToken.Into)
				throw new Exception("Into expected");
			scanner.Scan();

			if(scanner.Token != ScannerToken.Name)
				throw new Exception("Name expected");

			modificationEntity = scanner.GetString();			
			path = objectResolver.ParsePath(modificationEntity);
			scanner.Scan();

			if(scanner.Token != ScannerToken.LeftParen)
				throw new Exception("'(' Expected");

            List<ExpressionName> list = new List<ExpressionName>();
			do
			{
				scanner.Scan();

				if(scanner.Token != ScannerToken.Name)
					throw new Exception("Name expected");

				string name = scanner.GetString();
				scanner.Scan();
				
				if (scanner.Token == ScannerToken.Period)
				{
					scanner.Scan();
					if (scanner.Token != ScannerToken.Name)
						throw new Exception("name expected");
					string value = scanner.GetString();
					scanner.Scan();
					list.Add(new ExpressionName(name, value));
				}
				else
					list.Add(new ExpressionName("", name));
			}while(scanner.Token == ScannerToken.Comma);
			if(scanner.Token != ScannerToken.RightParen)
				throw new Exception("')' Expected");
			scanner.Scan();

            insertFields = list.ToArray();


			if(scanner.Token == ScannerToken.Select)
			{
				ObjectQuery oq = new ObjectQuery(objectResolver,  string.Empty);
				oq.ParseSelect(scanner);
				insertValues = oq;
			}
			else
			{
				if(scanner.Token != ScannerToken.Values)
					throw new Exception(" Values or select expected ");
				scanner.Scan();

				if(scanner.Token != ScannerToken.LeftParen)
					throw new Exception("'(' Expected");

                List<ExpressionNode> listValues = new List<ExpressionNode>();
				do
				{
					scanner.Scan();

					ExpressionNode expr = parser.ParseRowValueConstructor(scanner);
					listValues.Add(expr);					
				}while(scanner.Token == ScannerToken.Comma);

				if(scanner.Token != ScannerToken.RightParen)
					throw new Exception("')' Expected");
				scanner.Scan();

				insertValues = listValues.ToArray();
				
				if(scanner.Token != ScannerToken.End)
					throw new Exception("Unexpected token");
			}
		}

		public void ParseUpdate(Scanner scanner, params ScannerToken[] endTokens)
		{
			queryType = QueryType.Update;

			scanner.Scan();

			if(scanner.Token != ScannerToken.Name)
				throw new Exception("Name expected");

			modificationEntity = scanner.GetString();
            updatePath = objectResolver.ParsePath(modificationEntity);
			scanner.Scan();

			if(scanner.Token != ScannerToken.Set)
				throw new Exception("'Set' Expected");

			setExpressions = SetExpression.ParseSetExpressions(scanner, parser);

			string spath = modificationEntity;
			if(scanner.Token == ScannerToken.From)
			{
				spath = scanner.GetString(scanner.Pos, scanner.Text.Length - scanner.Pos - 1);
			}
			else
			{				
				spath = modificationEntity +" " + scanner.GetString() + " " + scanner.GetString(scanner.Pos, Math.Max(scanner.Text.Length - scanner.Pos - 1, 0));
			}
			path = objectResolver.ParsePath(spath);			
		}

		public void ParseDelete(Scanner scanner, params ScannerToken[] endTokens)
		{
			queryType = QueryType.Delete;

			scanner.Scan();
			
			if(scanner.Token != ScannerToken.Name)
				throw new Exception("Name expected");

			modificationEntity = scanner.GetString();
			scanner.Scan();

			updatePath = objectResolver.ParsePath(modificationEntity);
			
			string spath;
			if(scanner.Token == ScannerToken.From)
			{
				spath = scanner.GetString(scanner.Pos, scanner.Text.Length - scanner.Pos - 1);
			}
			else
			{				
				spath = modificationEntity +" " + scanner.GetString() + " " + scanner.GetString(scanner.Pos, Math.Max(scanner.Text.Length - scanner.Pos - 1, 0));
			}
			path = objectResolver.ParsePath(spath);
		}

		public void ParseSelect(Scanner scanner, params ScannerToken[] endTokens)
		{
			queryType = QueryType.Select;
			bool unionAll;
			do
			{
				unionAll = false;
				this.SelectColumns = null;
				this.Distinct = false;
				this.TopCount = Int64.MaxValue;
				this.SortOrder = null;

				if (scanner.Token == ScannerToken.Select)
				{
					scanner.Scan();
					if (scanner.Token == ScannerToken.Distinct)
					{
						this.Distinct = true;
						scanner.Scan();
					}

					if (scanner.Token == ScannerToken.Top)
					{
						scanner.Scan();
						if (scanner.Token != ScannerToken.Integer)
							throw new Exception("syntax error");
						this.TopCount = Int64.Parse(scanner.GetString());
						scanner.Scan();
					}

                    List<SelectExpression> list = new List<SelectExpression>();
					list.Add(SelectExpression.ParseSelectExpression(scanner));
					while(scanner.Token == ScannerToken.Comma)
					{
						scanner.Scan();
						list.Add(SelectExpression.ParseSelectExpression(scanner));
					}
					if (scanner.Token != ScannerToken.From)
						throw new Exception("syntax error");
					scanner.Scan();
                    this.SelectColumns = list.ToArray();
				}
				
				this.Path = ObjectPath.Parse(this.objectResolver, scanner, parser, externalPath);

				if (scanner.Token == ScannerToken.Group)
				{
					scanner.Scan();
					if (scanner.Token != ScannerToken.By)
						throw new Exception("'by' expected");

					this.GroupBy = GroupByExpression.ParseGroupBy(scanner);

					if (scanner.Token == ScannerToken.Having)
					{
						this.Having = HavingExpression.ParseHavingExpression(scanner);
					}
				}

				if (scanner.Token == ScannerToken.Order)
				{
					scanner.Scan();
					if (scanner.Token != ScannerToken.By)
						throw new Exception("'by' expected");
					this.SortOrder = SortExpression.ParseSortOrder(scanner);
				}
				if (scanner.Token == ScannerToken.Union)
				{
					scanner.Scan();
					if (scanner.Token != ScannerToken.All)
						throw new Exception("'all' expected");
					scanner.Scan();
					unionAll = true;

					if (this.SelectColumns != null)
					{
						foreach (SelectExpression se in this.SelectColumns)
							CheckSelectExpression(this.Path, se);
					}

					ObjectQuery ua = new ObjectQuery(
						this.Path,  this.SelectColumns, 
						this.Distinct,  this.SortOrder,  
						this.TopCount,  this.GroupBy
						, this.Having);
					ua.queryType = QueryType.Select;
					ua.Path = this.Path;
					List<ObjectQuery> al = this.alUnionAllObjectQueries;
					reinit();
					this.alUnionAllObjectQueries = al;
					this.alUnionAllObjectQueries.Add(ua);
				}
			}while(unionAll);

			if (scanner.Token != ScannerToken.End
				&& Array.IndexOf(endTokens, scanner.Token) == -1)
				throw new Exception("syntax error");

			if (this.SelectColumns != null)
			{
				foreach (SelectExpression se in this.SelectColumns)
					CheckSelectExpression(this.Path, se);
			}

			if(this.alUnionAllObjectQueries.Count > 0)
			{	
				ObjectQuery ua = new ObjectQuery(
					this.Path,  this.SelectColumns, 
					this.Distinct,  this.SortOrder,  
					this.TopCount,  this.GroupBy
					, this.Having);
				ua.Path = this.Path;
				List<ObjectQuery> al = this.alUnionAllObjectQueries;
				al.Add(ua);

				foreach(SelectExpression se in ((ObjectQuery) alUnionAllObjectQueries[0]).SelectColumns)
				{
					new PropertyData(VirtualEntityData, se);
				}
				this.reinit();
				this.alUnionAllObjectQueries = al;
			}
			else
			{
                if (SelectColumns != null)
                {
                    foreach (SelectExpression se in SelectColumns)
                    {
                        new PropertyData(VirtualEntityData, se);
                    }
                }
			}
		}

		private static void CheckSelectExpression(ObjectPath path, SelectExpression se)
		{
			// TODO: use a visitor on c.Expr

			ExpressionName en = se.Expr as ExpressionName;
			if (en != null)
			{
				string name;
				if (en.Qualifier == "")
				{
					name = en.Value;
					Correlation s = (Correlation)path.Correlations[name];
					if (s != null)
						return;
				}
				else if (en.Value == "*")
				{
					name = en.Qualifier + ".*";
					Correlation s = (Correlation)path.Correlations[en.Qualifier];
					if (s != null)
						return;
				}
				else 
				{
					name = en.Qualifier + "." + en.Value;
					Correlation s;
                    if (!path.Correlations.TryGetValue(en.Qualifier, out s))
						throw new Exception("Unknown correlation " + name);

					if( s.EntityData.PropertyInfos.ContainsKey(en.Value))
						return;
					if( s.EntityData.ReferenceInfos.ContainsKey(en.Value))
					{
						ReferenceData rd = s.EntityData.ReferenceInfos[en.Value];
						if(!rd.IsSingleReference && !s.NeededRelations.ContainsKey(rd))
						{
							s.NeededRelations.Add(rd, en);
						}
						return;
					}
					
					if(!s.NeededInverseRelations.Contains(en.Value))
						s.NeededInverseRelations.Add(en.Value);
					return;

				}
				//throw new Exception("invalid column " + name + " : column names should be either the name of an alias as defined in the from clause, in which case it represents the oid of the entity, or the name of a property, qualified by an alias (i.e. \"alias.property\")");
			}
		}

		private IObjectResolver objectResolver;
		private string text = "";
		private ObjectPath path;
		private ObjectPath updatePath;
		private ObjectPath externalPath;
		private SelectExpression[] selectColumns;
		private GroupByExpression[] groupBy;
		private HavingExpression having;
		private bool distinct;
		private SortExpression[] sortOrder;
		private Int64 topCount = Int64.MaxValue;
		private QueryType queryType;
		private string modificationEntity;
		private ExpressionName[] insertFields;
		private object insertValues;
		private SetExpression[] setExpressions;

		public SetExpression[] SetExpressions
		{
			get{ return setExpressions; }
		}

		public ExpressionName[] InsertFields
		{
			get{ return insertFields; }
		}

		public object InsertValues
		{
			get{ return insertValues; }
		}

		public string ModificationEntity
		{
			get{ return modificationEntity; }
		}

		public QueryType QueryType
		{
			get{ return queryType;}
		}

		public ObjectPath UpdatePath
		{
			get{ return updatePath;}
		}

		private void ExpressionParser_ParseSubQuery(object sender, Adoor.Expression.ExpressionParser.ParseSubQueryEventArgs e)
		{
			e.SourceQuery = this;
			
			ObjectQuery oq = new ObjectQuery();
			oq.Parse(e.Scanner, Adoor.Expression.ScannerToken.RightParen);
			e.SubQuery = oq;
		}
	}

	public enum QueryType
	{
		Select = 0,
		Insert = 1,
		Update = 2,
		Delete = 3
	}
}